using System;
using System.Globalization;
using System.Collections.Generic;

namespace Measurements
{
    public class TemperatureUnit : MeasurementUnit
    {   
        public TemperatureUnit(string name, string unit, ToBaseUnitDelegate toBaseUnit, FromBaseUnitDelegate fromBaseUnit)
            : base (name, unit, Temperature.Kelvin, toBaseUnit, fromBaseUnit) { }
    }
    
    public class Temperature : Measurement<Temperature, TemperatureUnit>, IFormattable
    { 
        public static TemperatureUnit Kelvin = new TemperatureUnit("Kelvin", "K", m => m, m => m);
        public static TemperatureUnit Celsius = new TemperatureUnit("Celsius", "°C", m => m + 273.15, m => m - 273.15);
        public static TemperatureUnit Fahrenheit = new TemperatureUnit("Fahrenheit", "°F", m => (m + 459.67) * 5 / 9, m => m * 9 / 5 - 459.67);
            
        public static IEnumerable<TemperatureUnit> Units
        {
            get
            {
                yield return Kelvin;
                yield return Celsius;
                yield return Fahrenheit;
            }
        }        
        
        public static class Water
        {
            public static Temperature BoilingPoint
            {
                get { return Temperature.Create(100, Temperature.Celsius); }                
            }
            
            public static Temperature FreezingPoint
            {
                get { return Temperature.Create(0, Temperature.Celsius); }   
            }
            
            public static Temperature BoilingPointAt(Length altitude)
            {
                if (altitude == null) throw new ArgumentNullException("altitude");
                                
                var hg = 29.921 * Math.Pow(1 - 6.8753 * 0.000001 * altitude[Length.Foot], 5.2559);
                var bp = 49.161 * Math.Log(hg) + 44.932;
                
                return Temperature.Create(bp, Temperature.Fahrenheit);
            }
        }        
        
        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <returns>A string representation of the temperature.</returns>
        public override string ToString()
        {
            return ToString(null, null);
        }
        
        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="format">
        /// A single format specifier that indicates how to format the value of this
        /// temperature. The format parameter can be "G", "C", "F", or "K". If format
        /// is null or the empty string (""), "G" is used.
        /// </param>
        /// <param name="provider">
        /// An IFormatProvider reference that supplies culture-specific formatting
        /// services.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        /// <exception cref="FormatException">
        /// The value of format is not null, the empty string (""), "G", "C", "F", or
        /// "K".
        /// </exception>
        public string ToString(string format, IFormatProvider provider)
        {
            if (String.IsNullOrEmpty(format)) format = "G";
            if (provider == null) provider = CultureInfo.CurrentCulture;

            switch (format.ToUpperInvariant())
            {
                case "G":
                case "C":
                    return this[Celsius].ToString("F2", provider) + " °C";
                case "F":
                    return this[Fahrenheit].ToString("F2", provider) + " °F";
                case "K":
                    return this[Kelvin].ToString("F2", provider) + " K";
                default:
                    throw new FormatException(String.Format("The {0} format string is not supported.", format));
            }
        }      
        
        /// <summary>
        /// Returns a string representation of the temperature value.
        /// </summary>
        /// <param name="format">
        /// A single format specifier that indicates how to format the value of this
        /// temperature. The format parameter can be "G", "C", "F", or "K". If format
        /// is null or the empty string (""), "G" is used.
        /// </param>
        /// <returns>A string representation of the temperature.</returns>
        /// <exception cref="FormatException">
        /// The value of format is not null, the empty string (""), "G", "C", "F", or
        /// "K".
        /// </exception>
        public string ToString(string format)
        {
            return ToString(format, null);
        }
        
        /// <summary>
        /// Finds the temperature change between the two temperatures using the specified <paramref name="unit"/>.
        /// </summary>
        /// <param name="value">The temperature to be subtracted from this temperature.</param>
        /// <param name="unit">The unit of measurement to use during the subtraction.</param>
        /// <returns>The change between the two temperatures, signed to indicate an increase or decrease in temperature.</returns>
        public double Change(Temperature other, TemperatureUnit unit)
        {
            return (this[unit] - other[unit]) * -1;
        }

        /// <summary>
        /// Finds the temperature change between the two temperatures using the Kelvin unit of measurement.
        /// </summary>
        /// <param name="value">The temperature to be subtracted from this temperature.</param>
        /// <returns>The change between the two temperatures, signed to indicate an increase or decrease in temperature.</returns>
        public double Change(Temperature other)
        {
            return Change(other, Temperature.Kelvin);
        }    
        
        /// <summary>
        /// Gets a temperature defined by the boiling point of H2O.
        /// </summary>
        public static Temperature BoilingPointOfWater { get { return Create(100, Temperature.Celsius); }
        }        
        
        public static Temperature FreezingPointOfWater
        {
            get
            {
                return Create(0, Temperature.Celsius);   
            }
        }
    }
}
